--
-- State: Player Name
--
local CattleProd_STATE_PLAYER_NAME = "PlayerName";

--
-- State: Player Realm
--
local CattleProd_STATE_PLAYER_REALM = "PlayerRealm";

--
-- State: Player Damage
--
local CattleProd_STATE_PLAYER_DAMAGE = "PlayerDamage";

--
-- State: Player Heal
--
local CattleProd_STATE_PLAYER_HEAL = "PlayerHeal";

--
-- State: Player Scoreboard
--
local CattleProd_STATE_PLAYER_SCOREBOARD = "PlayerScoreboard";

--
-- State: Player XY
--
local CattleProd_STATE_PLAYER_XY = "PlayerXY";

--
-- State: Player Defender
--
local CattleProd_STATE_PLAYER_DEFENDER = "PlayerDefender";

--
-- State: Player Deaths
--
local CattleProd_STATE_PLAYER_DEATHS = "PlayerDeaths";

--
-- State: Player Ghost
--
local CattleProd_STATE_PLAYER_GHOST = "PlayerGhost";

--
-- Players States array
-- Contains PlayerStates with the oldest first and the newest last.
--
CattleProd_PlayersStates = {};

--
-- AFK Players
--
CattleProd_AFKPlayers = {};

--
-- Players State Delta
--
CattleProd_PlayersStateDelta = {};

--
-- Zero Effort Players (playerKey to boolean)
--
CattleProd_ZeroEffortPlayers = {};

--
-- Known Leeches (playerKey => count)
--
CattleProd_KnownLeeches = {};

--
-- Get calculate player state interval.
--
-- return Interval
--
function CattleProd_GetCalculatePlayerStateInterval()
	local interval = CattleProd_GetSetting(
		CattleProd_CALCULATE_PLAYER_STATE_INTERVAL);

	return interval;
end

--
-- Get maximum player states size.
--
-- return Maximum players states size
--
function CattleProd_GetMaximumPlayersStatesSize()
	local requiredDataPoints = CattleProd_GetSetting(
		CattleProd_REQUIRED_DATA_POINTS);

	return requiredDataPoints;
end

--
-- CattleProd_GetBattlegroundScoreboardMap Function.
--
-- signature:
-- playerIndex Player Index
--
-- return Player Scoreboard Map
--
CattleProd_GetBattlegroundScoreboardMap_Function = nil;

--
-- CattelProd_GetBattlegroundDefenisblePositionsMap Function.
--
-- signature:
-- return Defensible Position map
--
-- map: Position => { X, Y }
--
CattleProd_GetBattlegroundDefensiblePositionsMap_Function = nil;

--
-- Get Battleground Defensive Range Function.
--
-- signature:
-- return Defensive Range
--
CattleProd_GetBattlegroundDefensiveRange_Function = nil;

--
-- Get Battleground Defender Effort Function.
--
-- signature:
-- defenderCount Defender Count
--
-- return Effort
--
CattleProd_CalculateBattlegroundDefenderEffort_Function = nil;

--
-- Create player state key.
--
-- playerName Player Name
-- playerRealm Player Realm
--
-- return Player Key
--
function CattleProd_PlayerKey(playerName, playerRealm)
	CattleProd_AssertType(playerName, "string", "Illegal Argument");

	local playerNameRealm = playerName .. "-";

	if (playerRealm == nil or # playerRealm == 0)
	then
		playerRealm = GetRealmName();
	end

	playerNameRealm = playerNameRealm .. playerRealm;

	return playerNameRealm;
end

--
-- Get player name from playerNameRealm.
--
-- playerNameRealm Player Name and Realm
--
-- return Player Name, Player Realm
--
function CattleProd_PlayerNameRealm(playerNameRealm)
	CattleProd_AssertType(playerNameRealm, "string", "Illegal Argument");

	local dashOffset;
	local playerName, playerRealm;
	
	dashOffset = string.find(playerNameRealm, "-");
	if (dashOffset == nil)
	then
		playerName = playerNameRealm;
		playerRealm = GetRealmName();
	else
		playerName = string.sub(playerNameRealm, 1, (dashOffset - 1));
		playerRealm = string.sub(playerNameRealm, (dashOffset + 1));
	end

	return playerName, playerRealm;
end

--
-- Get player map positions map.
--
-- return Player Map Positions map
--
function CattleProd_GetPlayerMapPositions()
	local playerMapPositions = {};

	local raidCount = GetNumRaidMembers();
	if (raidCount == 0)
	then
		return playerMapPositions;
	end

	for i = 1, raidCount
	do
		local raidMember = "raid" .. i;

		-- playerNameRealm
		local playerName, playerRealm = UnitName(raidMember);
		local playerKey = CattleProd_PlayerKey(playerName, playerRealm);

		-- position
		local playerX, playerY = GetPlayerMapPosition(raidMember);

		-- scale
		playerX = playerX * 100;
		playerY = playerY * 100;

		-- add to playerMapPositions
		playerMapPositions[playerKey] = { playerX, playerY };
	end

	return playerMapPositions;
end

--
-- Calculate if player is defending.
--
-- defensiblePositions Defensible Positions map
-- playerXY Player XY
--
-- return Boolean
--
function CattleProd_IsDefender(defensiblePositions, playerXY)
	CattleProd_AssertType(defensiblePositions, "table", "Illegal Argument");
	CattleProd_AssertType(playerXY, "table", "Illegal Argument");

	local defenderDistance = 1.00;
	if (CattleProd_GetBattlegroundDefensiveRange_Function ~= nil)
	then
		defenderDistance = CattleProd_GetBattlegroundDefensiveRange_Function();
	else
		CattleProd_Warn("defenderDistance: %.2f", defenderDistance);
	end

	for location, objectiveXY in pairs(defensiblePositions)
	do
		CattleProd_AssertType(objectiveXY, "table", "Illegal State");
		for i = 1, # objectiveXY, 2
		do
			local lengthX, lengthY;
			lengthX = objectiveXY[i + 0] - playerXY[1];
			lengthY = objectiveXY[i + 1] - playerXY[2];

			local distance;
			distance = math.sqrt((lengthX * lengthX) + (lengthY * lengthY));

			if (distance < defenderDistance)
			then
				return true;
			end
		end
	end

	return false;
end

--
-- Check if player is already marked AFK.
--
-- playerKey Player key
--
-- return Boolean
--
function CattleProd_AlreadyMarkedAFK(playerKey)
	CattleProd_AssertType(playerKey, "string", "Illegal Argument");

	local MAX_DEBUFFS = 40;
	for i = 1, MAX_DEBUFFS
	do
		local name, rank, icon, charges, debuffType, timemax, timeleft
		name, rank, icon, charges, debuffType, timemax, timeleft = UnitDebuff(playerKey, i);

		if (name == nil)
		then
			return false;
		end

		if (CattleProd_LocaleEqual(CattleProd_DEBUFF_IDLE, name) or 
			CattleProd_LocaleEqual(CattleProd_DEBUFF_INACTIVE, name))
		then
			return true;
		end
	end

	return false;
end

--
-- Get known leech count.
--
-- return Count
--
function CattleProd_KnownLeechCount()
	local knownLeechCount = 0;

	-- battlefield score data should be up to date
	local battlefieldScores = GetNumBattlefieldScores();
	if (battlefieldScores == 0)
	then
		CattleProd_Debug("battlefieldScores == 0");
		return knownLeechCount;
	end

	-- iterate over battlefield
	for playerIndex = 1, battlefieldScores
	do
		-- battlefield scores
		local player, killingBlows, honorKills, deaths, honorGained, faction, rank, race, class, filename, damage, heal;
		player, killingBlows, honorKills, deaths, honorGained, faction, rank, race, class, filename, damage, heal = GetBattlefieldScore(playerIndex);

		local playerName, playerRealm, playerKey
		playerName, playerRealm = CattleProd_PlayerNameRealm(player);
		playerKey = CattleProd_PlayerKey(playerName, playerRealm);
		
		local count = CattleProd_KnownLeeches[playerKey];
		if (count ~= nil)
		then
			if (count >= knownLeechCount)
			then
				knownLeechCount = knownLeechCount + 1;
			end
		end

	end	-- loop
	
	return knownLeechCount;
end

--
-- Report Known Leeches
--
function CattleProd_ReportKnownLeeches()
	-- settings
	local reportKnownLeeches = CattleProd_GetSetting(CattleProd_REPORT_KNOWN_LEECHES);
	local knownLeechCount = CattleProd_GetSetting(CattleProd_KNOWN_LEECH_COUNT);

	-- report known leeches?
	if (reportKnownLeeches == false)
	then
		return;
	end;

	-- report known leeches
	CattleProd_Report(CattleProd_REPORT_LEVEL_MEDIUM, "Reporting Known Leeches");

	CattleProd_AssertType(CattleProd_KnownLeeches, "table", "Illegal State");

	-- battlefield score data should be up to date
	local battlefieldScores = GetNumBattlefieldScores();
	if (battlefieldScores == 0)
	then
		CattleProd_Debug("battlefieldScores == 0");
		return;
	end

	-- iterate over battlefield
	for playerIndex = 1, battlefieldScores
	do
		-- battlefield scores
		local player, killingBlows, honorKills, deaths, honorGained, faction, rank, race, class, filename, damage, heal;
		player, killingBlows, honorKills, deaths, honorGained, faction, rank, race, class, filename, damage, heal = GetBattlefieldScore(playerIndex);

		local playerName, playerRealm, playerKey
		playerName, playerRealm = CattleProd_PlayerNameRealm(player);
		playerKey = CattleProd_PlayerKey(playerName, playerRealm);
		
		local count = CattleProd_KnownLeeches[playerKey];
		if (count ~= nil)
		then
			if (count >= knownLeechCount)
			then
				-- print to screen
				CattleProd_Report(CattleProd_REPORT_LEVEL_LOW,
					"Known Leech: %s Count: %d", playerKey, count);

				-- report player afk
				CattleProd_ReportPlayerIsPVPAFK(playerKey);
			end
		end

	end	-- loop
end

--
-- Calculate players states effort and report players who are detected to
-- be afkers.
--
function CattleProd_CalculatePlayersStatesEffort()
	CattleProd_AssertNotNil(CattleProd_PlayersStates, "Illegal State");

	if (# CattleProd_PlayersStates < CattleProd_GetMaximumPlayersStatesSize())
	then
		local interval = CattleProd_GetCalculatePlayerStateInterval();
		local requiredStates = CattleProd_GetMaximumPlayersStatesSize();

		local remainingSeconds = (requiredStates * interval) - (# CattleProd_PlayersStates * interval);
		
		CattleProd_Report(CattleProd_REPORT_LEVEL_HIGH, "Minimum required data in %d seconds", remainingSeconds);
		return;
	end

	-- calculate average damage and heal
	local damageAverage, healAverage;
	damageAverage, healAverage = CattleProd_CalculatePlayerStatesAverageDamageHeal();
	CattleProd_AssertType(damageAverage, "number", "Unable to calculate damage average");
	CattleProd_AssertType(healAverage, "number", "Unable to calculate heal average");
	
	local totalEffort = 0;
	local playerEfforts = {};
	CattleProd_PlayersStateDelta = {};

	-- list of players to iterate over
	local playersStateAlpha = CattleProd_PlayersStates[1];
	for playerKey, playerState in pairs(playersStateAlpha)
	do
		-- calculate player states delta			
		local playerStates = CattleProd_GetPlayerStates(playerKey);
		CattleProd_AssertNotNil(playerStates, "Unable to get player states");

		local playerStatesDelta = CattleProd_CalculatePlayerStateDelta(playerStates);
		if (playerStatesDelta ~= nil)
		then
			-- calculate player effort
			local playerEffort = CattleProd_CalculatePlayerStateDeltaEffort(
				playerStatesDelta, damageAverage, healAverage);
			CattleProd_AssertType(playerEffort, "number", "Unable to calculate player effort");

			-- store player states delta
			CattleProd_PlayersStateDelta[playerKey] = playerStatesDelta;
	
			-- update total effort
			totalEffort = totalEffort + playerEffort;

			-- zero effort players
			local zeroEffort = CattleProd_ZeroEffortPlayers[playerKey];			
			if (playerEffort == 0 and zeroEffort == nil)
			then
				-- flag player as potential leech
				CattleProd_ZeroEffortPlayers[playerKey] = true;
				CattleProd_Report(CattleProd_REPORT_LEVEL_HIGH,
					"Flagging Zero Effort Player: %s", playerKey);
			elseif (playerEffort == 0 and zeroEffort ~= nil)
			then
				-- keep player as zero effort
			else
				-- unflag player as leech
				if (zeroEffort ~= nil)
				then
					CattleProd_Report(CattleProd_REPORT_LEVEL_HIGH,
						"Unflagging Zero Effort Player: %s", playerKey);
					CattleProd_ZeroEffortPlayers[playerKey] = false;
				end

				-- decrement known leech count for each time
				-- a known leech actually performs so they can
				-- be unknown as a leech				
				local count = CattleProd_KnownLeeches[playerKey];
				if (count ~= nil)
				then
					count = count - 1;
					if (count == 0)
					then
						CattleProd_KnownLeeches[playerKey] = nil;
					else
						CattleProd_KnownLeeches[playerKey] = count;
					end
				end
			end
	
			-- add to player effort order
			CattleProd_AddPlayerEffort(playerEfforts, playerKey, playerEffort);
		end
	end

	-- cannot divide by zero in
	-- average effort calculation
	if (# playerEfforts == 0)
	then
		CattleProd_Debug("Missing Data: # playerEfforts == 0");
		return;
	end
	
	-- calculate average effort
	local averageEffort = totalEffort / # playerEfforts;
	
	-- clear afker players
	CattleProd_AFKPlayers = {};

	local interval = CattleProd_GetCalculatePlayerStateInterval();
	local dataPoints = CattleProd_GetMaximumPlayersStatesSize();
	local minutes = (interval * dataPoints) / 60;		
		
	for i = 1, # playerEfforts
	do
		local playerEffortArray = playerEfforts[i];
		local playerKey = playerEffortArray[1];
		local playerEffort = playerEffortArray[2];

		-- player state delta
		local playerStateDelta = CattleProd_PlayersStateDelta[playerKey];
		CattleProd_AssertType(playerStateDelta, "table", "Illegal State");

		-- individual player state delta
		local playerDamage = playerStateDelta[CattleProd_STATE_PLAYER_DAMAGE];
		CattleProd_AssertType(playerDamage, "number");
		
		local playerHeal = playerStateDelta[CattleProd_STATE_PLAYER_HEAL];
		CattleProd_AssertType(playerHeal, "number");

		local playerDefender = playerStateDelta[CattleProd_STATE_PLAYER_DEFENDER];
		CattleProd_AssertType(playerDefender, "number");

		local playerScoreboardCount = 0;		
		local playerScoreboard = playerStateDelta[CattleProd_STATE_PLAYER_SCOREBOARD];
		if (playerScoreboard ~= nil)
		then
			CattleProd_AssertType(playerScoreboard, "table", "Illegal State");
			for column, count in pairs(playerScoreboard)
			do
				playerScoreboardCount = playerScoreboardCount + count;
			end
		end

		local name, realm = CattleProd_PlayerNameRealm(playerKey);

		if (CattleProd_IsLeech(averageEffort, playerEffort))
		then
			-- already marked?
			if (CattleProd_AlreadyMarkedAFK(playerKey))
			then
				CattleProd_Report(CattleProd_REPORT_LEVEL_MEDIUM,
					"MARKED %s Effort: %d Dam: %d Heal: %d Obj: %d Def: %.2d%%",
					name, playerEffort, playerDamage, playerHeal, playerScoreboardCount,
					((playerDefender * 100) / dataPoints) );
			else
				-- add to afkers to report in battleground
				table.insert(CattleProd_AFKPlayers, playerKey);

				-- report players AFK
				CattleProd_ReportPlayerIsPVPAFK(playerKey);

				-- create message
				CattleProd_Report(CattleProd_REPORT_LEVEL_LOW,
					"LEECH %s Effort: %d Dam: %d Heal: %d Obj: %d Def: %.2d%%",
					name, playerEffort, playerDamage, playerHeal, playerScoreboardCount,
					((playerDefender * 100) / dataPoints) );
			end
		else
			CattleProd_Report(CattleProd_REPORT_LEVEL_HIGH,
				"PLAYER %s Effort: %d Dam: %d Heal: %d Obj: %d Def: %.2d%%",
				name, playerEffort, playerDamage, playerHeal, playerScoreboardCount,
				((playerDefender * 100) / dataPoints) );
		end
	end

	-- report summary
	CattleProd_Report(CattleProd_REPORT_LEVEL_LOW,
		"CattleProd %d Minute Report: Total Leeches: %d Average Effort: %d",
		minutes, # CattleProd_AFKPlayers, averageEffort);
end

--
-- AFK player count.
--
-- return Count
--
function CattleProd_AFKPLayerCount()
	return # CattleProd_AFKPlayers;
end

--
-- Announce AFKers to Battleground
--
-- afkers AFKers
--
function CattleProd_AnnounceInBattleground()
	
	if (# CattleProd_AFKPlayers > 0)
	then
		-- maximum to report
		local maxReport = CattleProd_GetSetting(CattleProd_ANNOUNCE_TOP_N_LEECHERS);
		maxReport = math.min(maxReport, # CattleProd_AFKPlayers);

		-- timing
		local interval = CattleProd_GetCalculatePlayerStateInterval();
		local dataPoints = CattleProd_GetMaximumPlayersStatesSize();
		local minutes = (interval * dataPoints) / 60;		

		-- summary
		local summary =
			string.format("CattleProd %d Minute Report: Total Leeches: %d Top %d Leeches:",
				minutes, # CattleProd_AFKPlayers, maxReport);
		SendChatMessage(summary, "BATTLEGROUND");
		
		for i = 1, # CattleProd_AFKPlayers
		do
			-- only announce worst offenders
			if (i > maxReport)
			then
				return;
			end
		
			-- player key
			local playerKey = CattleProd_AFKPlayers[i];
			CattleProd_AssertType(playerKey, "string", "Illegal State");

			-- player state delta
			local playerStateDelta = CattleProd_PlayersStateDelta[playerKey];
			CattleProd_AssertType(playerStateDelta, "table", "Illegal State");

			-- individual player state delta
			local playerDamage = playerStateDelta[CattleProd_STATE_PLAYER_DAMAGE];
			CattleProd_AssertType(playerDamage, "number");
			
			local playerHeal = playerStateDelta[CattleProd_STATE_PLAYER_HEAL];
			CattleProd_AssertType(playerHeal, "number");

			local playerDefender = playerStateDelta[CattleProd_STATE_PLAYER_DEFENDER];
			CattleProd_AssertType(playerDefender, "number");

			local playerScoreboardCount = 0;		
			local playerScoreboard = playerStateDelta[CattleProd_STATE_PLAYER_SCOREBOARD];
			if (playerScoreboard ~= nil)
			then
				CattleProd_AssertType(playerScoreboard, "table", "Illegal State");
				for column, count in pairs(playerScoreboard)
				do
					playerScoreboardCount = playerScoreboardCount + count;
				end
			end

			local name, realm = CattleProd_PlayerNameRealm(playerKey);

			-- create message
			local message =
				string.format("%s Dam: %d Heal: %d Obj: %d Def: %.2d%%",
					name, playerDamage, playerHeal, playerScoreboardCount,
					((playerDefender * 100) / dataPoints) );

			-- report to battleground
			SendChatMessage(message, "BATTLEGROUND");
		end -- loop
	end
end

--
-- Add { playerKey, playerEffort } to playerEfforts array.
--
-- playerEfforts Player Efforts array
-- playerKey Player Key
-- playerEffort Player Effort
--
function CattleProd_AddPlayerEffort(playerEfforts, playerKey, playerEffort)
	CattleProd_AssertType(playerEfforts, "table", "Illegal Argument");
	CattleProd_AssertType(playerKey, "string", "Illegal Argument");
	CattleProd_AssertType(playerEffort, "number", "Illegal Argument");

	local added = false;
	
	for i = 1, # playerEfforts
	do
		local playerEffortArray = playerEfforts[i];
		if (added == false and playerEffort < playerEffortArray[2])
		then
			table.insert(playerEfforts, i, { playerKey, playerEffort });
			added = true;
		end
	end

	if (added == false)
	then
		table.insert(playerEfforts, { playerKey, playerEffort });
	end
end

--
-- Add playersState map to the PlayersStates array.
--
-- playersState Players State map
--
-- return Count
--
function CattleProd_AddPlayersState(playersState)
	CattleProd_AssertType(CattleProd_PlayersStates, "table", "Illegal State");
	CattleProd_AssertType(playersState, "table", "Illegal Argument");

	-- insert entry
	table.insert(CattleProd_PlayersStates, playersState);

	-- remove any playersState entries
	-- beyond the maximum
	while (# CattleProd_PlayersStates > CattleProd_GetMaximumPlayersStatesSize())
	do
		table.remove(CattleProd_PlayersStates, 1);
	end

	return # CattleProd_PlayersStates;
end

--
-- Clear PlayersStates array.
--
-- return Cleared Count
--
function CattleProd_ClearPlayersStates()
	-- clear players states
	CattleProd_PlayersStates = {};

	-- clear AFK players
	CattleProd_AFKPlayers = {};
	
	-- clear players state delta
	CattleProd_PlayersStateDelta = {};
	
	-- add to known leeches
	CattleProd_Report(CattleProd_REPORT_LEVEL_HIGH, "Adding Known Leeches");
	for playerKey, zeroEffort in pairs(CattleProd_ZeroEffortPlayers)
	do
		if (zeroEffort == true)
		then
			-- update known leeches count
			local count = CattleProd_KnownLeeches[playerKey];
			if (count == nil)
			then
				CattleProd_KnownLeeches[playerKey] = 1;
				CattleProd_Report(CattleProd_REPORT_LEVEL_MEDIUM,
					"Adding Known Leech: %s", playerKey); 
			else
				count = count + 1;
				CattleProd_KnownLeeches[playerKey] = count;
				CattleProd_Report(CattleProd_REPORT_LEVEL_HIGH,
					"Updating Known Leech: %s Count: %d", playerKey, count);
			end
		end
	end
	
	-- clear zero effort players1
	CattleProd_ZeroEffortPlayers = {};
end

--
-- Tear down state.
--
function CattleProd_TearDownState()
	-- player scoreboard function
	CattleProd_GetBattlegroundScoreboardMap_Function = nil;

	-- get defensible positions map
	CattleProd_GetBattlegroundDefensiblePositionsMap_Function = nil;

	-- get battleground player scoreboard effort
	CattleProd_CalculateBattlegroundScoreboardEffort_Function = nil;

	-- get battleground defensive range
	CattleProd_GetBattlegroundDefensiveRange_Function = nil;

	-- calculate battleground defender effort
	CattleProd_CalculateBattlegroundDefenderEffort_Function = nil;

	-- clear states
	CattleProd_ClearPlayersStates();
end

--
-- Print PlayersStates playerStates maps.
--
function CattleProd_PrintPlayersStates()
	CattleProd_AssertType(CattleProd_PlayersStates, "table", "Illegal State");

	CattleProd_Message("PlayerStates: %d", # CattleProd_PlayersStates);

	for i = 1, # CattleProd_PlayersStates
	do
		local playersState = CattleProd_PlayersStates[i];
		CattleProd_AssertType(playersState, "table", "Illegal State");

		CattleProd_Message("PlayersState[%d]:", i);
		CattleProd_PrintPlayersState(playersState);
	end
end

--
-- Calculate PlayersStates average damage and heal.
--
-- return Average Damage, Average Heal
--
function CattleProd_CalculatePlayerStatesAverageDamageHeal()
	CattleProd_AssertType(CattleProd_PlayersStates, "table", "Illegal State");
	
	if (# CattleProd_PlayersStates == 0 or # CattleProd_PlayersStates == 1)
	then
		return 0, 0;
	end

	local playersStateAlpha = CattleProd_PlayersStates[1];
	local playersStateOmega = CattleProd_PlayersStates[# CattleProd_PlayersStates];
	
	local damageAlpha, healAlpha, damageCountAlpha, healCountAlpha;
	damageAlpha, healAlpha, damageCountAlpha, healCountAlpha = CattleProd_CalculatePlayersStateTotalDamageHeal(playersStateAlpha);

	local damageOmega, healOmega, damageCountOmega, healCountOmega;
	damageOmega, healOmega, damageCountOmega, healCountOmega = CattleProd_CalculatePlayersStateTotalDamageHeal(playersStateOmega);

	local damageDelta, healDelta;
	damageDelta = damageOmega - damageAlpha;
	healDelta = healOmega - healAlpha;
	
	local dammageCount = ((damageCountAlpha + damageCountOmega) / 2);
	local healCount = ((healCountAlpha + healCountOmega) / 2);

	local damageAverage, healAverage;
	damageAverage = damageDelta / dammageCount;
	healAverage = healDelta / healCount;

	return damageAverage, healAverage;
end

--
-- Print playersState playerState objects.
--
-- playersState Players State
--
function CattleProd_PrintPlayersState(playersState)
	CattleProd_AssertType(playersState, "table", "Illegal Argument");

	local count = 0;

	-- iterate through player state objects and print them
	for playerKey, playerState in pairs(playersState)
	do
		CattleProd_AssertType(playerState, "table", "Illegal State");
		CattleProd_PrintPlayerState(playerState);
		count = count + 1;
	end
	
	if (count == 0)
	then
		CattleProd_Warn("0 players state");
	end
end

--
-- Calculate Player State
--
-- Calculate players current state by iterating through all of players
-- and getting their name, realm, damage, heal, scoreboard data, location,
-- and defender status.  The playersState map is then stored in the
-- PlayersStates array.
--
function CattleProd_CalculatePlayersState()
	-- current playersState
	local playersState = {};

	-- defensible positions
	local defensiblePositions = nil;
	if (CattleProd_GetBattlegroundDefensiblePositionsMap_Function ~= nil)
	then
		defensiblePositions = CattleProd_GetBattlegroundDefensiblePositionsMap_Function();
	end

	-- player map positions
	local playerMapPositions = CattleProd_GetPlayerMapPositions();

	-- faction
	local myFaction = CattleProd_GetFaction();

	local battlefieldScores = GetNumBattlefieldScores();
	if (battlefieldScores == 0)
	then
		CattleProd_Debug("battlefieldScores == 0");
		return;
	end

	-- iterate over battlefield
	for playerIndex = 1, battlefieldScores
	do
		-- battlefield scores
		local player, killingBlows, honorKills, deaths, honorGained, faction, rank, race, class, filename, damage, heal;
		player, killingBlows, honorKills, deaths, honorGained, faction, rank, race, class, filename, damage, heal = GetBattlefieldScore(playerIndex);

		if (myFaction == faction)
		then
			local playerName, playerRealm, playerKey
			playerName, playerRealm = CattleProd_PlayerNameRealm(player);
			playerKey = CattleProd_PlayerKey(playerName, playerRealm);

			-- scoreboard stats
			local scoreboard;
			if (CattleProd_GetBattlegroundScoreboardMap_Function ~= nil)
			then
				scoreboard = CattleProd_GetBattlegroundScoreboardMap_Function(playerIndex);
			else
				scoreboard = {};
			end

			-- location stats
			local playerXY = playerMapPositions[playerKey];
			if (playerXY == nil)
			then
				CattleProd_Debug("playerXY == nil, playerKey: %s", playerKey);
				playerXY = { 0.0, 0.0 };
			end

			-- defender stats
			local defender = false;
			if (defensiblePositions ~= nil)
			then
				defender = CattleProd_IsDefender(defensiblePositions, playerXY);
			end

			-- ghost
			local ghost = CattleProd_UnitIsGhost(playerKey);

			-- player state
			local playerState;
			playerState = CattleProd_CreatePlayerState(playerName, playerRealm, damage, heal, scoreboard, playerXY, defender, deaths, ghost);

			-- playersState
			CattleProd_AddPlayerState(playersState, playerKey, playerState);
		end -- faction
	end -- loop

-- TODO: remove me
--CattleProd_PrintPlayersState(playersState);

    -- add new playersState to PlayersStates array
    CattleProd_AddPlayersState(playersState);
end

--
-- Calculate playersState total damage.
--
-- playersState Players State
--
-- return Damage Total, Heal Total, Damage Count, Heal Count
--
function CattleProd_CalculatePlayersStateTotalDamageHeal(playersState)
	CattleProd_AssertType(playersState, "table", "Illegal Argument");

	local damageTotal = 0;
	local healTotal = 0;
	local damageCount = 0;
	local healCount = 0;

	-- iterate over all the player state objects
	for key, playerState in pairs(playersState)
	do
		local damage, heal;
		damage, heal = CattleProd_CalculatePlayerStateDamageHeal(playerState);

		CattleProd_AssertType(damage, "number", "Unable to calculate player state damage");
		if (damage > 0)
		then
			damageCount = damageCount + 1;
			damageTotal = damageTotal + damage;
		end

		CattleProd_AssertType(heal, "number", "Unable to calculate player state heal");
		if (heal > 0)
		then
			healCount = healCount + 1;
			healTotal = healTotal + heal;
		end
	end
	
	return damageTotal, healTotal, damageCount, healCount;
end

--
-- Add playerState map to playersState map.
--
-- playersState Players State map
-- playerKey Player Key
-- playerState Player State map
--
function CattleProd_AddPlayerState(playersState, playerKey, playerState)
	CattleProd_AssertType(playersState, "table", "Illegal Argument");
	CattleProd_AssertType(playerKey, "string", "Illegal Argument");
	CattleProd_AssertType(playerState, "table", "Illegal Argument");

	playersState[playerKey] = playerState;
end

--
-- Create state object.
--
-- name Player Name
-- realm Player Realm
-- damage Player Damage
-- heal Player Heal
-- scoreboard Scoreboard Map
-- xy Player XY
-- defender Defender
-- playerDeaths Deaths
-- playerGhost Ghost
--
-- return Player State
--
function CattleProd_CreatePlayerState(playerName, playerRealm, playerDamage, playerHeal, playerScoreboard, playerXY, playerDefender, playerDeaths, playerGhost)
	local playerState = {};
	
	if (playerName == nil)
	then
		CattleProd_Warn("playerName == nil");
	else
		playerState[CattleProd_STATE_PLAYER_NAME] = playerName;
	end

	if (playerRealm == nil)
	then
		CattleProd_Warn("playerRealm == nil");
	else
		playerState[CattleProd_STATE_PLAYER_REALM] = playerRealm;
	end

	if (playerDamage == nil)
	then
		CattleProd_Warn("playerDamage == nil");
		playerState[CattleProd_STATE_PLAYER_DAMAGE] = 0;
	else
		playerState[CattleProd_STATE_PLAYER_DAMAGE] = playerDamage;
	end

	if (playerHeal == nil)
	then
		CattleProd_Warn("playerHeal == nil");
		playerState[CattleProd_STATE_PLAYER_HEAL] = 0;
	else
		playerState[CattleProd_STATE_PLAYER_HEAL] = playerHeal;
	end

	if (playerScoreboard == nil)
	then
		CattleProd_Warn("playerScoreboard == nil");
		playerState[CattleProd_STATE_PLAYER_SCOREBOARD] = {};
	else
		playerState[CattleProd_STATE_PLAYER_SCOREBOARD] = playerScoreboard;
	end

	if (playerXY == nil)
	then
		CattleProd_Warn("playerXY == nil");
		playerState[CattleProd_STATE_PLAYER_XY] = { 0.0, 0.0 };
	else
		if (type(playerXY) ~= "table")
		then
			CattleProd_Error("type(playerXY) == %s", type(playerXY));
			playerState[CattleProd_STATE_PLAYER_XY] = { 0.0, 0.0 };
		else
			playerState[CattleProd_STATE_PLAYER_XY] = playerXY;
		end
	end

	if (playerDefender == nil)
	then
		CattleProd_Warn("playerDefender == nil");
		playerState[CattleProd_STATE_PLAYER_DEFENDER] = false;
	else
		if (type(playerDefender) ~= "boolean")
		then
			CattleProd_Error("type(playerDefender) == %s", type(playerDefender));
			playerState[CattleProd_STATE_PLAYER_DEFENDER] = false;
		else
			playerState[CattleProd_STATE_PLAYER_DEFENDER] = playerDefender;
		end
	end

	if (playerDeaths == nil)
	then
		CattleProd_Warn("playerDeaths == nil");
		playerState[CattleProd_STATE_PLAYER_DEATHS] = 0;
	else
		if (type(playerDeaths) ~= "number")
		then
			CattleProd_Error("type(playerDeaths) == %s", type(playerDeaths));
			playerState[CattleProd_STATE_PLAYER_DEATHS] = 0;
		else
			playerState[CattleProd_STATE_PLAYER_DEATHS] = playerDeaths;
		end
	end

	if (playerGhost == nil)
	then
		CattleProd_Warn("playerGhost == nil");
		playerState[CattleProd_STATE_PLAYER_GHOST] = false;
	else
		if (type(playerGhost) ~= "boolean")
		then
			CattleProd_Error("type(playerGhost) == %s", type(playerGhost));
			playerState[CattleProd_STATE_PLAYER_GHOST] = false;
		else
			playerState[CattleProd_STATE_PLAYER_GHOST] = playerGhost;
		end
	end

	return playerState;
end

--
-- Calculate playerState damage and heal.
--
-- playerState Player State
--
-- return Damage, Heal
--
function CattleProd_CalculatePlayerStateDamageHeal(playerState)
	CattleProd_AssertType(playerState, "table", "Illegal Argument");
	
	local damage, heal;
	damage = playerState[CattleProd_STATE_PLAYER_DAMAGE];
	if (damage == nil)
	then
		CattleProd_Warn("damage == nil");
		damage = 0;
	end
	
	heal = playerState[CattleProd_STATE_PLAYER_HEAL];
	if (heal == nil)
	then
		CattleProd_Warn("heal == nil");
		damage = 0;
	end
	
	return damage, heal;
end

--
-- Print playerState.
--
-- playerState Player State
--
function CattleProd_PrintPlayerState(playerState)
	CattleProd_AssertType(playerState, "table", "Illegal Argument");

	local message = "PlayerState: ";

	local playerName = playerState[CattleProd_STATE_PLAYER_NAME];
	if (playerName == nil)
	then
		CattleProd_Error("playerName == nil");
		message = message .. "nil,";
	else
		message = message .. playerName .. ",";
	end

	local playerRealm = playerState[CattleProd_STATE_PLAYER_REALM];
	if (playerRealm == nil)
	then
		CattleProd_Error("playerRealm == nil");
		message = message .. "nil,";
	else
		message = message .. playerRealm .. ",";
	end

	local playerDamage = playerState[CattleProd_STATE_PLAYER_DAMAGE];
	if (playerDamage == nil)
	then
		CattleProd_Error("playerDamage == nil");
		message = message .. "nil,";
	else
		message = message .. format("%6d", playerDamage) .. ",";
	end

	local playerHeal = playerState[CattleProd_STATE_PLAYER_HEAL];
	if (playerHeal == nil)
	then
		CattleProd_Error("playerHeal == nil");
		message = message .. "nil,";
	else
		message = message .. format("%6d", playerHeal) .. ",";
	end

	local playerScoreboard = playerState[CattleProd_STATE_PLAYER_SCOREBOARD];
	if (playerScoreboard == nil)
	then
		CattleProd_Error("playerScoreboard == nil");
		message = message .. "nil,";
	else
		if (type(playerScoreboard) ~= "table")
		then
			CattleProd_Error("type(playerScoreboard) == %s", type(playerScoreboard));
			message = message .."nottable,";
		else
			message = message .."notnil,";
		end
	end

	local playerXY = playerState[CattleProd_STATE_PLAYER_XY];
	if (playerXY == nil)
	then
		CattleProd_Error("playerXY == nil");
		message = message .. "nil,nil,";
	else
		if (type(playerXY) ~= "table")
		then
			CattleProd_Error("type(playerXY) == %s", type(playerXY));
			message = "nottable,nottable,";
		else
			if (# playerXY ~= 2)
			then
				CattleProd_Error("# playerXY == %d", # playerXY);
			end

			for i = 1, # playerXY
			do
				message = message .. format("%2.1f", playerXY[i]) .. ",";
			end
		end
	end

	local playerDefender = playerState[CattleProd_STATE_PLAYER_DEFENDER];
	if (playerDefender == nil)
	then
		CattleProd_Error("playerDefender == nil");
		message = message .. "nil,";
	else
		message = message .. tostring(playerDefender) .. ",";
	end

	CattleProd_Message(message);
end

--
-- Get player states.
--
-- playerKey Player Key
--
-- return Player States array
--
function CattleProd_GetPlayerStates(playerKey)
	CattleProd_AssertType(CattleProd_PlayersStates, "table", "Illegal State");
	CattleProd_AssertType(playerKey, "string", "Illegal Argument");
	
	local playerStates = {};
	local playersState, playerState;

	for i = 1, # CattleProd_PlayersStates
	do
		playersState = CattleProd_PlayersStates[i];

		-- it is possible a player joined late and
		-- does not have an entry in the ealier states
		-- or a player has left and does not have an
		-- entry in the later states.
		playerState = playersState[playerKey];
		if (playerState ~= nil)
		then
			table.insert(playerStates, playerState);
		end
	end

	return playerStates;
end

--
-- Calculate Player State Delta from playerStates.
--
-- playerStates Player States array
--
-- return Player State Delta
--
function CattleProd_CalculatePlayerStateDelta(playerStates)
	CattleProd_AssertType(playerStates, "table", "Illegal Argument");

	local playerStateDelta = {};

	-- player states count
	local playerStatesCount = # playerStates;
	if (playerStatesCount < CattleProd_GetMaximumPlayersStatesSize())
	then
		-- may be early or player may have left
		return nil;
	end

	-- player state alpha and omega
	local playerStateAlpha, playerStateOmega;

	playerStateAlpha = playerStates[1];
	CattleProd_AssertType(playerStateAlpha, "table");
	
	playerStateOmega = playerStates[playerStatesCount];
	CattleProd_AssertType(playerStateOmega, "table");
	
	-- player name and realm
	local playerName = playerStateAlpha[CattleProd_STATE_PLAYER_NAME];
	CattleProd_AssertType(playerName, "string");

	playerStateDelta[CattleProd_STATE_PLAYER_NAME] = playerName;
	
	local playerRealm = playerStateAlpha[CattleProd_STATE_PLAYER_REALM];
	CattleProd_AssertType(playerRealm, "string");
	playerStateDelta[CattleProd_STATE_PLAYER_REALM] = playerRealm;

	-- calculate player damage and heal deltas	
	local playerDamageAlpha, playerHealAlpha
	playerDamageAlpha = playerStateAlpha[CattleProd_STATE_PLAYER_DAMAGE];
	playerHealAlpha = playerStateAlpha[CattleProd_STATE_PLAYER_HEAL];

	local playerDamageOmega, playerHealOmega
	playerDamageOmega = playerStateOmega[CattleProd_STATE_PLAYER_DAMAGE];
	playerHealOmega = playerStateOmega[CattleProd_STATE_PLAYER_HEAL];

	local playerDamageDelta, playerDamageDelta
	playerDamageDelta = playerDamageOmega - playerDamageAlpha;
	playerHealDelta = playerHealOmega - playerHealAlpha;

	playerStateDelta[CattleProd_STATE_PLAYER_DAMAGE] = playerDamageDelta;
	playerStateDelta[CattleProd_STATE_PLAYER_HEAL] = playerHealDelta;

	-- calculate player battleground scoreboard delta
	local playerScoreboardAlpha;
	local playerScoreboardOmega;

	playerScoreboardAlpha = playerStateAlpha[CattleProd_STATE_PLAYER_SCOREBOARD];
	playerScoreboardOmega = playerStateOmega[CattleProd_STATE_PLAYER_SCOREBOARD];

	if (playerScoreboardAlpha ~= nil and playerScoreboardOmega ~= nil)
	then
		local playerScoreboardDelta = {};
		for column, x in pairs(playerScoreboardAlpha)
		do
			-- calculate scoreboard column delta
			local playerScoreboardColumnAlpha, playerScoreboardColumnOmega;
			playerScoreboardColumnAlpha = playerScoreboardAlpha[column];
			playerScoreboardColumnOmega = playerScoreboardOmega[column];
			
			local playerScoreboardColumnDelta;
			playerScoreboardColumnDelta = playerScoreboardColumnOmega - playerScoreboardColumnAlpha;

			-- store scoreboard column delta
			playerScoreboardDelta[column] = playerScoreboardColumnDelta;
		end

		-- store scoreboard delta
		playerStateDelta[CattleProd_STATE_PLAYER_SCOREBOARD] = playerScoreboardDelta;
	end
	
	-- calculate player defender delta
	local playerDefenderDelta = 0;
	-- calculate player ghost
	local playerGhostDelta = 0;
	for i = 1, playerStatesCount
	do
		-- ghost?
		local playerGhost = false;
		playerGhost = playerStates[i][CattleProd_STATE_PLAYER_GHOST];
		
		if (playerGhost == nil)
		then
			CattleProd_Error("playerGhost == nil");
		elseif (playerGhost)
		then
			playerGhostDelta = playerGhostDelta + 1;
		end

		-- defender?
		local playerDefender = false;
		playerDefender = playerStates[i][CattleProd_STATE_PLAYER_DEFENDER];
		
		if (playerDefender == nil)
		then
			CattleProd_Error("playerDefender == nil");
		elseif (playerDefender)
		then
			-- we only give defender credit if player
			-- is not a ghost
			if (not playerGhost)
			then
				playerDefenderDelta = playerDefenderDelta + 1;
			end
		end
	end
	
	playerStateDelta[CattleProd_STATE_PLAYER_DEFENDER] = playerDefenderDelta;
	playerStateDelta[CattleProd_STATE_PLAYER_GHOST] = playerGhostDelta;

	-- calculate player deaths
	local playerDeathsAlpha = playerStateAlpha[CattleProd_STATE_PLAYER_DEATHS];
	local playerDeathsOmega = playerStateOmega[CattleProd_STATE_PLAYER_DEATHS];
	local playerDeathsDelta = playerDeathsOmega - playerDeathsAlpha;

	playerStateDelta[CattleProd_STATE_PLAYER_DEATHS] = playerDeathsDelta;
	

	return playerStateDelta;
end

--
-- Print scoreboard data.
--
-- scoreboard Scoreboard map
--
function CattleProd_PrintScoreboard(scoreboard)
	CattleProd_AssertType(scoreboard, "table", "Illegal Argument");

	for column, value in pairs(scoreboard)
	do
		CattleProd_Message(format("column: %d value: %d", column, value));
	end
end

--
-- Print Player States Delta.
--
-- playerStatesDelta Player States Delta
--
function CattleProd_PrintPlayerStatesDelta(playerStatesDelta)
	CattleProd_AssertType(playerStatesDelta, "table", "Illegal Argument");

	local message = "PlayerStatesDelta:";
	
	-- player name
	local playerName = playerStatesDelta[CattleProd_STATE_PLAYER_NAME];
	if (playerName == nil)
	then
		CattleProd_Error("playerName == nil");
		message = message .. " nil,";
	else
		message = message .. " " .. playerName .. ",";
	end

	-- player realm
	local playerRealm = playerStatesDelta[CattleProd_STATE_PLAYER_REALM];
	if (playerRealm == nil)
	then
		CattleProd_Error("playerRealm == nil");
		message = message .. " nil,";
	else
		message = message .. " " .. playerRealm .. ",";
	end

	-- player damage
	local playerDamage = playerStatesDelta[CattleProd_STATE_PLAYER_DAMAGE];
	if (playerDamage == nil)
	then
		CattleProd_Error("playerDamage == nil");
		message = message .. " nil,";
	else
		message = message .. " " .. format("%d", playerDamage) .. ",";
	end

	-- player heal
	local playerHeal = playerStatesDelta[CattleProd_STATE_PLAYER_HEAL];
	if (playerHeal == nil)
	then
		CattleProd_Error("playerHeal == nil");
		message = message .. " nil,";
	else
		message = message .. " " .. format("%d", playerHeal) .. ",";
	end

	-- player scoreboard
	local playerScoreboard = playerStatesDelta[CattleProd_STATE_PLAYER_SCOREBOARD];
	if (playerScoreboard == nil)
	then
		CattleProd_Error("playerScoreboard == nil");
		message = message .. " nil,";
	else
		if (type(playerScoreboard) ~= "table")
		then
			CattleProd_Error("type(playerScoreboard) ~= table");
		else
			for column, value in pairs(playerScoreboard)
			do
				message = message .. " " .. column .. "=" .. format("%d", value) .. ",";
			end
		end
	end

	-- player defender
	local playerDefender = playerStatesDelta[CattleProd_STATE_PLAYER_DEFENDER];
	if (playerDefender == nil)
	then
		CattleProd_Error("playerDefender == nil");
		message = message .. " nil,";
	else
		message = message .. " " .. format("%d", playerDefender) .. ",";
	end
	
	CattleProd_Message(message);
end

--
-- Calculate player state delta effort.
--
-- playerStateDelta Player State Delta
-- averageDamage Average Damage
-- averageHeal Average Heal
--
-- return Effort
--
function CattleProd_CalculatePlayerStateDeltaEffort(playerStateDelta, averageDamage, averageHeal)
	CattleProd_AssertType(playerStateDelta, "table", "Illegal Argument");
	CattleProd_AssertType(averageDamage, "number", "Illegal Argument");
	CattleProd_AssertType(averageHeal, "number", "Illegal Argument");

	local totalEffort = 0;

	-- calculate damage and healing effort
	local damage = playerStateDelta[CattleProd_STATE_PLAYER_DAMAGE];
	if (damage == nil)
	then
		CattleProd_Error("damage == nil");
		return nil;
	elseif (damage < 0)
	then
		CattleProd_Error("damage < 0: %d", damage);
		return nil;
	end
	
	local heal = playerStateDelta[CattleProd_STATE_PLAYER_HEAL];
	if (heal == nil)
	then
		CattleProd_Error("heal == nil");
		return nil;
	elseif (heal < 0)
	then
		CattleProd_Error("heal < 0: %d", heal);
		return nil;
	end
	
	local scaledDamage, scaledHeal;
	scaledDamage, scaledHeal = CattleProd_ScaleDamageAndHealing(averageDamage, averageHeal, damage, heal);

	totalEffort = totalEffort + (scaledDamage / 2);
	totalEffort = totalEffort + (scaledHeal / 2);

	-- calculate scoreboard effort
	if (CattleProd_CalculateBattlegroundScoreboardEffort_Function ~= nil)
	then
		local playerScoreboardDelta = playerStateDelta[CattleProd_STATE_PLAYER_SCOREBOARD];
		if (playerScoreboardDelta == nil)
		then
			CattleProd_Warn("playerScoreboardDelta == nil");
		else
			local scoreboardEffort = CattleProd_CalculateBattlegroundScoreboardEffort_Function(playerScoreboardDelta);
			if (scoreboardEffort == nil)
			then
				CattleProd_Warn("scoreboardEffort == nil");
			else
				totalEffort = totalEffort + scoreboardEffort;
			end
		end
	end

	-- calculate defender effort
	if (CattleProd_CalculateBattlegroundDefenderEffort_Function ~= nil)
	then
		local playerDefender = playerStateDelta[CattleProd_STATE_PLAYER_DEFENDER];
		if (playerDefender == nil)
		then
			CattleProd_Error("playerDefender == nil");
		else
			local interval = CattleProd_GetCalculatePlayerStateInterval();
			local defenderEffort = CattleProd_CalculateBattlegroundDefenderEffort_Function(playerDefender);

			-- scale effort to per minute
			defenderEffort = defenderEffort * (interval / 60);
			
			totalEffort = totalEffort + defenderEffort;
		end
	end 
	
	return totalEffort;
end
